/**
 * @projectDescription X-Cube: Ajax Library
 * @author Julio Greff
 * @version 0.1
 */

/**
 * Classe X-Cube. Contém todas as funções para requisições XMLHttpRequest
 * @param {String} url Url a ser solicitada
 * @param {Object} options Opcional, objeto contendo opções
 * @return {Object} Objeto X-Cube
 */
var XCube = function(url, options) {
	/**
	 * Opções padrão caso não sejam passadas como argumento
	 */
	this.options = {
		"method": "get",
		"data": "",
		"autoCancel": true,
		"async": true,
		"onComplete": empty,
		"onRequest": empty,
		"onCancel": empty,
		"onFailure": empty,
		"cache": false,
		"localCache": false,
		"history": false,
		"changeLinks": true,
		"update": false
	}
	/**
	 * Fila de requisições
	 */
	this.queue = [];
	/**
	 * Lista de Headers
	 */
	this.headers = {"X-Requested-With": "XMLHttpRequest"};
	/**
	 * Cache local de requisições
	 */
	this.localCache = {};
	/**
	 * Define headers para a requisição
	 * @param {String} headerName Nome do header
	 * @param {String} headerValue Valor do header
	 */
	this.setHeader = function(headerName, headerValue) {
		this.headers[headerName] = headerValue;
		return this;
	}
	/**
	 * Escolhe entre opções passadas como parâmetros e opções padrão. Usada internamente.
	 * @param {Object} options Opções para sobrescrever as opções padrão
	 */
	this.setOptions = function(options) {
		for(var option in options) {
			this.options[option] = options[option];
		}
		return this;
	}
	/**
	 * Define um objeto XMLHttpRequest. Usado internamente
	 */
	this.setXmlHttp = function() {
		this.XmlHttp = (window.XMLHttpRequest) ? new XMLHttpRequest() : (ActiveXObject ? new ActiveXObject('Microsoft.XMLHTTP') : false);
		return this;
	}
	/**
	 * Verifica as mudanças de estado da requisição. Usada internamente
	 * @param {String} data Dados a serem enviados
	 */
	this.onStateChange = function(data) {
		if(this.XmlHttp.readyState == 4) {
			if(this.XmlHttp.status == 200) {
				if(this.options.localCache == true) {
					this.localCache[escape(data)] = {
						"responseText": this.XmlHttp.responseText,
						"responseXML": this.XmlHttp.responseXML
					}
				}
				if(this.options.update != false) document.getElementById(this.options.update).innerHTML = this.XmlHttp.responseText;
				this.options.onComplete(this.XmlHttp.responseText, this.XmlHttp.responseXML);
				this.running = false;
				setTimeout(this.executeQueue.bind(this), 100);
			}
			else
				this.options.onFailure(this.XmlHttp.status, this.XmlHttp.statusText);
		}
		return this;
	}
	/**
	 * Executa a fila de requisições. Usada internamente
	 */
	this.executeQueue = function() {
		if(this.queue.length > 0)
			this.request(this.queue.shift());
		return this;
	}
	/**
	 * Inicia uma requisição
	 * @param {String} data Dados a serem enviados
	 */
	this.request = function(data) {
		if(this.options.autoCancel == true)
			this.stop();
		else if(this.running == true) {
			this.queue.push(data);
			return this;
		}
		var url = this.url;
		data = data || this.options.data || null;
		if(this.options.method == "get" && data) {
			url = this.url + ((this.url.indexOf("?") < 0) ? "?" : "&" ) + data;
			data = null
		}
		this.running = true;
		if(this.options.localCache == true && this.localCache[escape(data || url)]) {
			this.options.onComplete(this.localCache[escape(data || url)].responseText, this.localCache[escape(data || url)].responseXML);
			this.running = false;
			this.executeQueue();
			return this;
		}
		this.XmlHttp.open(this.options.method, url, this.options.async);
		this.XmlHttp.onreadystatechange = this.onStateChange.bind(this, data || url);
		for(var header in this.headers) this.XmlHttp.setRequestHeader(header, this.headers[header]);
		this.options.onRequest();
		this.XmlHttp.send(data);
		return this;
	}
	/**
	 * Pára uma requisição em andamento
	 */
	this.stop = function() {
		if(this.running == false)
			return this;
		this.running = false;
		this.XmlHttp.abort();
		this.XmlHttp.onreadystatechange = function(){};
		this.setXmlHttp();
		this.options.onCancel();
		return this;
	}
	/**
	 * Modifica os links para apontarem para a função request
	 * @param {Object} parentElement Elemento pai onde a função deve ser executada
	 */
	this.changeUrls = function(parentElement) {
		var links = (parentElement || document).getElementsByTagName("a");
		for(var link = 0; link < links.length; link++) {
			var url = (href = links[link].getAttribute("href"));
			if(url.indexOf(this.url) >= 0 && links[link].getAttribute("rel") == "async") {
				if(this.options.history == true) {
					links[link].setAttribute("href", "#" + this.id + "$" + href.substring(href.indexOf("?") + 1));
				}
				else {
					var request = this.request.bind(this);
					links[link].onclick = function() { request(this.getAttribute("href").substring(href.indexOf("?") + 1)); return false; };
				}
			}
		}
		return this;
	}
	
	this.setXmlHttp().setOptions(options);
	this.url = url;
	if((this.options.method = this.options.method.toLowerCase()) == "post")
		this.setHeader("Content-Type", "application/x-www-form-urlencoded")
	if(this.options.cache == false) {
		this.setHeader("Pragma", "no-cache");
		this.setHeader("Cache-Control", "no-cache")
	}
	if(this.options.history == true) {
		if(!History.register) History = new History()
		this.id = History.register(this.request.bind(this));
	}
	if(this.options.changeLinks == true)
		this.changeUrls();
	return this;
}

/**
 * Faz uma requisição simples
 * @param {String} url URL a ser requisitada
 * @param {String} update Elemento a ser atualizado com o resultado da requisição
 * @param {Object} options Opções a serem passadas
 */
XCube.simpleRequest = function(url, update, options) {
	var request = new XCube(url, options);
	request.setOptions({update: update, changeLinks: false});
	request.request();
}

/**
 * Classe History. Controla o histórico do navegador
 */
var History = function() {
	/**
	 * Cria um iframe (Hack para IE)
	 */
	this.createIframe = function() {
		if(this.isIe == false) return this;
		var iFrame = document.createElement("iframe");
		iFrame.setAttribute("src", "control.htm?id=" + window.location.hash.substring(1));
		iFrame.setAttribute("id", "control-iframe");
		iFrame.style.display = "none";
		document.body.appendChild(iFrame);
		return this;
	}
	/**
	 * Faz a checagem inicial para bookmarks
	 */
	this.checkBookmark = function() {
		return window.location.hash = ((browserHash = this.getHash()) == "") ? this.appDefault : window.location.hash;
	}
	/**
	 * Verifica mudanças no histórico
	 */
	this.handleHistory = function() {
		if((browserHash = this.getHash()) != this.appHash || this.getId() != this.appId) {
			if(browserHash != "") {
				this.appHash = browserHash;
				this.appId = this.getId();
				this.makeHistory(browserHash, this.appId);
			}
		}
		return this;
	}
	/**
	 * Cria uma nova entrada no histórico
	 * @param {String} newHash Hash da nova entrada
	 * @param {String} fnId ID da função
	 */
	this.makeHistory = function(newHash, fnId) {
		if(this.isIe == true)
			document.getElementById("control-iframe").setAttribute("src", "control.htm?id=" + this.getId() + "$" + newHash);
		else
			window.location.hash = this.getId() + "$" + newHash;
		this.appFunctions[fnId](newHash);
		return this;
	}
	/**
	 * Busca o hash do bavegador
	 * @return {String} Hash do navegador
	 */
	this.getHash = function() {
		return (hash = window.location.href.split("#")[1]) ? hash.split("$")[1] : "";
	}
	/**
	 * Busca a ID da função no Hash
	 * @return {String} ID da função
	 */
	this.getId = function() {
		return (hash = window.location.href.split("#")[1]) ? hash.split("$")[0] : 0;
	}
	/**
	 * Registra uma nova função
	 * @param {Object} fn Função a ser registrada
	 * @return {String} ID da função registrada
	 */
	this.register = function(fn) {
		var id = this.appFunctions.length;
		this.appFunctions[id] = fn;
		return id;
	}
	this.appHash = "";
	this.appId = 0;
	this.appFunctions = [];
	if(this.isIe = navigator.userAgent.toLowerCase().indexOf("msie") > -1)
		this.createIframe();
	this.checkInterval = setInterval(this.handleHistory.bind(this), 100);
	return this;
}

var empty = function(){}

/**
 * Vincula funções a determinado escopo
 * @param {Function} scope Escopo a ser vinculado
 * @param {Object} arguments Argumentos a serem passados à função
 * @return {Function} Função vinculada
 */
Function.prototype.bind = function() {
	var self = this, args = $A(arguments), scope = args.shift();
	return function() {
		return self.apply(scope, args.concat($A(arguments)));
	}
}
/**
 * Transforma objetos ou coleções em Arrays
 * @param {Object} object Objeto a ser transformado em Array
 * @return {Array} Objeto transformado em Array
 */
var $A = function(object) {
	var array = [];
	for(var i = 0; i < object.length; i++)
		array[i] = object[i];
	return array;
}